package demo.java.lang;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;

/**
 * 类装载器：JVM使用一个类，首先将该类的字节码装载进来，类装载器负责根据一个类的名称来定位，并生成类的字节码数据后返回给JVM。
 * 最常见的类加载器是将要加载的类名转换成一个.class文件名，然后从文件系统中找到该文件并读取其中的内容。
 * 这种类装载器也不是直接将.class文件中的内容原封不动地返回给java虚拟机，它需要将.class文件中的内容转换成Java虚拟机使用的类字节码，
 * 例如，Java程序中的字符串编译成.class文件后是以UTF-8编码存在的，而装载进java虚拟机后要被转换成Unicode编码。
 *
 * 类装载器本身也是一个Java类，允许开发人员自己编写自己的类装载器，以便通过其他各种特殊方式来产生类字节码。
 * 当一个类被加载后，JVM将其编译为可执行代码存储在内存中，并将索引信息存储进一个HashTable中，其索引关键字在HashTable中查找相应的信息，
 * 如果该代码已经存在，虚拟机直接从内存里调用该可执行代码，反之则调用类装载器并进行加载和编译。
 *
 * 类装载器装载某个类的字节码的过程实际上就是在创建Class类的一个实例对象，可以采用下面三种方式 获得：
 * <li>1、类名.class , 例如：System.class
 * <li>2、对象.getClass(), 例如：new Date().getClass()
 * <li>3、Class.forName(“类名”), 例如：Class.forName(“java.util.Date”);
 * 类装载器本身也是一个java类，java类库中提供了一个java.lang.ClassLoader来作为类装载器的基类，JVM和程序都调用ClassLoader的子类。
 * Class类中定义了一个getClassLoader方法，用于返回它所描述的类的类装载器对象，这个返回对象的类型就是ClassLoader.
 * <p>
 *
 * <p>
 * 类的加载的步骤：
 * <ol>
 * <li>加载 loading
 * <li>验证 verification
 * <li>准备 preparation (给类中的静态变量分配存储空间)
 * <li>解析 resolution
 * <li>初始化 initialization 。
 * </ol>
 * <p>
 * 有且只有以下四种情况必须立即对类进行”初始化”(称为对一个类进行主动引用)：
 * <li>遇到new、getstatic、putstatic、invokestatic这四条字节码指令时(使用new实例化对象的时候、读取或设置一个类的静态字段、调用一个类的静态方法)。
 * <li>使用java.lang.reflet包的方法对类进行反射调用的时候。
 * <li>当初始化一个类的时候，如果发现其父类没有进行过初始化，则需要先触发其父类的初始化。
 * <li>当虚拟机启动时，虚拟机会初始化主类(包含main方法的那个类)。
 * <p>
 * 被动引用：
 * <li>通过子类引用父类的静态字段，不会导致子类初始化(对于静态字段，只有直接定义这个字段的类才会被初始化)。
 * <li>通过数组定义类应用类：ClassA [] array=new ClassA[10]。触发了一个名为[LClassA的类的初始化，它是一个由虚拟机自动生成的、直接继承于Object的类，创建动作由字节码指令newarray触发。
 * <li>常量会在编译阶段存入调用类的常量池。
 * <p>
 * 验证：确保Class文件的字节流中包含的信息符合当前虚拟机的要求，并且不会危害虚拟机自身的安全。虚拟机规范：如果验证到输入的字节流不符合Class文件的存储格式，就抛出一个java.lang.VerifyError异常或其子类异常。
 * <li>文件格式验证：验证字节流是否符合Class文件格式的规范，并且能被当前版本的虚拟机处理。这个阶段的验证时给予字节流进行的，经过了这个阶段的验证之后，字节流才会进入内存的方法区中进行存储所以后面的验证阶段都是给予方法区的存储结构进行的。
 * <li>元数据验证：对类的元数据信息进行语义校验，保证不存在不符合java语言规范的元数据信息。
 * <li>字节码验证：进行数据流和控制流分析，对类的方法体进行校验分析，保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
 * <li>符号引用验证：发生在虚拟机将符号引用转化为直接引用的时候(解析阶段)，对常量池中的各种符号引用的信息进行匹配性的校验。
 * <p>
 * 解析:在虚拟机将常量池内的符号引用替换为直接引用的过程。
 * <li>符号引用：符号引用以一组符号来描述所引用的目标，符号可以是任何形式的字面量，只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关，引用的目标并不一定已经加载到内存中。
 * <li>直接引用：直接引用可以是直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。如果有了直接引用，那引用的目标必定已经在内存中存在。
 * <p>
 * 初始化:对静态变量和静态代码块执行初始化工作
 * <ol>
 * <li><clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的，编译器收集的顺序是由语句在源文件中出现的顺序决定的。
 * 静态语句块只能访问到定义在静态语句块之前的变量，定义在它之后的变量，在前面的静态语句块中可以赋值，但是不能访问。
 *
 * <li>方法与实例构造器< init>()不同，不需要显示的调用父类构造器，虚拟机会保证在子类的< clinit>()方法执行之前，父类的< clinit>()已经执行完毕。
 *
 * <li>< clinit>()方法对于类或接口来说不是必须的，如果一个类中没有静态语句块也没有对变量的赋值操作，那么编译器可以不为这个类生成< clinit>()方法。
 *
 * <li>执行接口的< clinit>()不需要先执行父接口的< clinit>()方法，只有当父接口中定义的变量被使用时，父接口才会被初始化。接口的实现类在初始化时也不会执行接口的< clinit>()方法。
 *
 * <li>虚拟机会保证一个类的< clinit>()方法在多线程环境中被正确的加锁和同步，如果多个线程同时去初始化一个类，则只会有一个线程去执行这个类的< clinit>()方法，其他线程需要阻塞等待。
 * </ol>
 */
public class ClassLoaderDemo {

    static final Logger logger = LoggerFactory.getLogger(ClassLoaderDemo.class);

    /**
     * 类加载双亲委派机制:JVM在加载类时默认采用的是双亲委派机制。
     * 通俗的讲，就是某个特定的类加载器在接到加载类的请求时，首先将加载任务委托给父类加载器，依次递归，如果父类加载器可以完成类加载任务，就成功返回；
     * 只有父类加载器无法完成此加载任务时，才自己去加载。
     * <p>
     * 使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。
     * 例如类java.lang.Object，它存在在rt.jar中，无论哪一个类加载器要加载这个类，最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载，
     * 因此Object类在程序的各种类加载器环境中都是同一个类。
     * 相反，如果没有双亲委派模型而是由各个类加载器自行加载的话，如果用户编写了一个java.lang.Object的同名类并放在ClassPath中，
     * 那系统中将会出现多个不同的Object类，程序将混乱。
     * 因此，如果开发者尝试编写一个与rt.jar类库中重名的Java类，可以正常编译，但是永远无法被加载运行。
     * <p>
     * 双亲委派模型的系统实现在java.lang.ClassLoader的loadClass()方法中，先检查是否已经被加载过，若没有加载则调用父类加载器的loadClass()方法，
     * 若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败，则抛出ClassNotFoundException异常后，再调用自己的findClass()方法进行加载。
     */
    public void 双亲委派机制() {
    }

    /**
     * 从Java虚拟机的角度来说，只存在两种不同的类加载器：
     * <li>一种是启动类加载器（Bootstrap ClassLoader），这个类加载器使用C++语言实现（HotSpot虚拟机中），是虚拟机自身的一部分；
     * <li>另一种就是所有其他的类加载器，这些类加载器都有Java语言实现，独立于虚拟机外部，并且全部继承自java.lang.ClassLoader。
     * <p><br>
     * 从开发者的角度，类加载器可以细分为：
     * <li>启动（Bootstrap）类加载器：负责将 Java_Home/lib下面的类库加载到内存中（比如rt.jar）。
     * 由于引导类加载器涉及到虚拟机本地实现细节，开发者无法直接获取到启动类加载器的引用，所以不允许直接通过引用进行操作。
     *
     * <li>标准扩展（Extension）类加载器：是由 Sun 的 ExtClassLoader（sun.misc.Launcher$ExtClassLoader）实现的。
     * 它负责将Java_Home /lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
     *
     * <li>应用程序（Application）类加载器：是由 Sun 的 AppClassLoader（sun.misc.Launcher$AppClassLoader）实现的。
     * 它负责将系统类路径（CLASSPATH）中指定的类库加载到内存中。开发者可以直接使用系统类加载器。
     * 由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值，因此一般称为系统（System）加载器。
     * <p>
     * 除此之外，还有自定义的类加载器，它们之间的层次关系被称为类加载器的双亲委派模型。
     * 该模型要求除了顶层的启动类加载器外，其余的类加载器都应该有自己的父类加载器，
     * 而这种父子关系一般通过组合（Composition）关系来实现，而不是通过继承（Inheritance）
     * <p>
     * 双亲委派模型不是一种强制性约束，也就是你不这么做也不会报错怎样的，它是一种JAVA设计者推荐使用类加载器的方式。
     * JDBC就是如此，在rt.jar里面定义了这个SPI，那mysql有mysql的jdbc实现，oracle有oracle的jdbc实现，反正我java不管你内部如何实现的，反正你们都得统一按我这个来，
     * 这样我们java开发者才能容易的调用数据库操作。所以因为这样那就不得不违反这个约束啊，Bootstrap ClassLoader就得委托子类来加载数据库厂商们提供的具体实现。
     * 因为它的手只能摸到<JAVA_HOME>\lib中，其他的它无能为力。这就违反了自下而上的委托机制了。
     * Java就搞了个线程上下文类加载器，通过setContextClassLoader()默认情况就是应用程序类加载器然后Thread.current.currentThread().getContextClassLoader()获得类加载器来加载。
     */
    @Test
    public void 类加载器种类() {
        // 应用程序（Application）类加载器
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(appClassLoader);
        // 标准扩展（Extension）类加载器
        ClassLoader extClassLoader = appClassLoader.getParent();
        System.out.println(extClassLoader);
        // 启动（Bootstrap）类加载器
        ClassLoader bootstrapClassloader = extClassLoader.getParent();
        System.out.println(bootstrapClassloader);
    }

    @Test
    public void test() {

        System.out.println("JRE的扩展目录:" + System.getProperty("java.ext.dirs"));
        ClassLoader extensionClassloader = ClassLoader.getSystemClassLoader().getParent();
        System.out.println("the parent of extension classloader : " + extensionClassloader.getParent());
        System.out.println("java.class.path = " + System.getProperty("java.class.path"));

        System.out.println("System.class.getClassLoader() = " + System.class.getClassLoader());


    }

    public static void main(String[] args) throws Exception {
        // 自定义类加载器
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(fileName);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException();
                }
            }
        };

        String className = ClassLoaderDemo.class.getName();

        // 使用ClassLoaderTest的类加载器加载本类
        Object obj1 = ClassLoaderDemo.class.getClassLoader().loadClass(className).newInstance();
        System.out.println(obj1.getClass());
        System.out.println(obj1 instanceof ClassLoaderDemo);

        // 使用自定义类加载器加载本类
        Object obj2 = myLoader.loadClass(className).newInstance();
        System.out.println(obj2.getClass());
        System.out.println(obj2 instanceof ClassLoaderDemo);
    }

    /**
     * 通过动态生成类来模拟 “PermGen space”的内存溢出：
     * 即通过类加载的机制，不断加载新的类到内存区域中，在JDK7和JDK8中，我们分别设置永久代和元空间的大小，我们发现此时内存溢出的分别是永久代和元空间，也就说在JDK8中，方法区的实现已经由永久代转变成了元空间。
     *
     * <li>JDK7:java -XX:PermSize=8m -XX:MaxPermSize=8m demo.java.lang.ClassLoaderDemo
     * <li>JDK8:java -XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m demo.java.lang.ClassLoaderDemo
     */
    @Test
    public void metaSpaceOomMock() {
        List<ClassLoader> classLoaderList = new ArrayList<>();
        try {
            URL url = new File("/tmp").toURI().toURL();
            URL[] urls = {url};
            while (true) {
                ClassLoader classLoader = new URLClassLoader(urls);
                classLoaderList.add(classLoader);
                classLoader.loadClass("demo.java.lang.ClassLoaderDemo");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 当一个 JVM启动的时候，Java缺省开始使用如下三种类型类装入器：
     *
     * <li>Bootstrap加载器：引导类装入器是用本地代码实现的类装入器，它负责将/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。
     * 由于引导类加载器涉及到虚拟机本地实现细节，开发者无法直接获取到启动类加载器的引用，所以不允许直接通过引用进行操作。
     * <li>Extension加载器：扩展类加载器是由Sun的ExtClassLoader（sun.misc.Launcher$ExtClassLoader）实现的。
     * 它负责将<Java_Runtime_Home>/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
     * <li>System类加载器： 系统类加载器是由Sun的AppClassLoader（sun.misc.Launcher$AppClassLoader）实现的。
     * 它负责将系统类路径java-classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。
     * <li>用户自定义类加载器：也可以用来加载应用类。使用自定义的类加载器有很多特殊的原因：运行时重新加载类或者把加载的类分隔为不同的组，典型的用法比如 web服务器 Tomcat。
     * <p>
     * <br>
     * * 当我们在命令行输入java Xxx（某个类）时候，java内部会做些什么动作呢？ 流程如下：
     * <ol>
     * <li>1. 找到JRE；
     * <li>2. 找到JVM.dll；
     * <li>3. 启动JVM，并进行初始化；
     * <li>4. 产生Bootstrap Loader；
     * <li>5. 载入ExtClassLoader；（Ext – Extended）
     * <li>6. 载入AppClassLoader；
     * <li>7. 加载Xxx类。
     * </ol>
     * 类加载的时候遵循一个原则：“类加载器会依类的继承体系从上至下依次加载”。
     * <p>
     * 举个例子： “如果C继承了B并实现了接口I，而B有继承自A”，则类加载器在加载C时，加载的次序会是A B I C，（注：interface会如同class一样被Java编译器编译为独立的.class文件）
     */
    @Test
    public void classLoader() {
        ClassLoader clsLoader = Thread.currentThread().getContextClassLoader();

        C c = new C();

        try {
            Class<?> cls1 = Class.forName("demo.vo.UserVO");
            logger.info("{}", cls1);

            Class<?> cls2 = Class.forName("demo.vo.UserVO", true, Thread.currentThread().getContextClassLoader());
            logger.info("{}", cls2);
        } catch (ClassNotFoundException e) {
            logger.error("", e);
        }
    }

    /**
     * <h1>加载（Loading）</h1>是这样一个过程： 找到代表这个类的 class文件或根据特定的名字找到接口类型，然后读取到一个字节数组中。
     * <p>
     * 接着，这些字节会被解析检验它们是否代表一个 Class 对象并包含正确的major、minor 版本信息。
     * <p>
     * 直接父类的类和接口也会被加载进来。这些操作一旦完成，类或者接口对象就从二进制表示中创建出来了。
     */
    public void loading() {
    }

    /**
     * <h1>链接（Linking）</h1>是校验类或接口并准备类型和父类父接口的过程。 链接过程包含三步： 校验（verifying）、准备（preparing）、部分解析（optionally resolving）
     *
     * <h2>校验</h2>会确认类或者接口表示是否结构正确，以及是否遵循 Java 语言和 JVM 的语义要求，比如会进行下面的检查：
     * <li>1、格式一致且格式化正确的符号表；
     * <li>2、final 方法和类没有被重载；
     * <li>3、方法遵循访问控制关键词；
     * <li>4、方法参数的数量、类型正确；
     * <li>5、字节码没有不当的操作栈数据；
     * <li>6、变量在读取之前被初始化过；
     * <li>7、变量值的类型正确。
     * <p>
     * 在验证阶段做这些检查意味着不需要在运行阶段做这些检查。链接阶段的检查减慢了类加载的速度，但是它避免了执行这些字节码时的多次检查。
     * <h2>准备</h2>过程包括为静态存储和 JVM 使用的数据结构（比如方法表）分配内存空间。静态变量创建并初始化为默认值，但是初始化代码不在这个阶段执行，因为这是初始化过程的一部分。
     * <h2>解析</h2>是可选的阶段。它包括通过加载引用的类和接口来检查这些符号引用是否正确。如果不是发生在这个阶段，符号引用的解析要等到字节码指令使用这个引用的时候才会进行。
     */
    public void linking() {
    }

    /**
     * 共享类数据（CDS）是Hotspot JVM 5.0 的时候引入的新特性。在 JVM 安装过程中，安装进程会加载一系列核心 JVM 类（比如 rt.jar）到一个共享的内存映射区域。
     * CDS减少了加载这些类需要的时间，提高了JVM 启动的速度，允许这些类被不同的 JVM 实例共享，同时也减少了内存消耗。
     */
    public void cds共享类数据() {
    }

    ;

    interface I {
    }

    class A {
        public A() {
            System.out.println("实例化A");
        }
    }

    class B extends A {
        public B() {
            System.out.println("实例化B");
        }
    }

    class C extends B implements I {
        public C() {
            System.out.println("实例化C");
        }
    }
}
